package com.nulldev.util.web.HttpServer3.extensions.websocket;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;

import com.nulldev.util.JVM.JVM;
import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.VariableAPI.ByteUtil;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.data.Charsets.CharsetUtil;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.web.HttpServer3.HttpServer3;
import com.nulldev.util.web.HttpServer3.extensions.websocket.exceptions.BufferOverflow;
import com.nulldev.util.web.HttpServer3.extensions.websocket.exceptions.InvalidFrameException;
import com.nulldev.util.web.HttpServer3.extensions.websocket.exceptions.WebsocketException;
import com.nulldev.util.web.HttpServer3.extensions.websocket.frame.BasicFrame;
import com.nulldev.util.web.HttpServer3.extensions.websocket.frame.BasicFrameReader;
import com.nulldev.util.web.HttpServer3.extensions.websocket.frame.BasicFrameWriter;
import com.nulldev.util.web.HttpServer3.extensions.websocket.frame.Frame;
import com.nulldev.util.web.HttpServer3.extensions.websocket.frame.FrameType;
import com.nulldev.util.web.HttpServer3.extensions.websocket.frame.frames.CloseFrame;
import com.nulldev.util.web.HttpServer3.extensions.websocket.frame.frames.PongFrame;
import com.nulldev.util.web.HttpServer3.extensions.websocket.util.CloseReason;
import com.nulldev.util.web.HttpServer3.extensions.websocket.util.SubArrayCollection;

public abstract class Peer implements Runnable {
	private static final Logger log = LoggerFactory.getLogger();

	private final Socket socket;
	private final InputStream is;
	private final BasicFrameReader bfr;
	private final BasicFrameWriter bfw;
	private final OutputStream os;
	private final UUID peerID;
	private final Map<String, String> headers;

	public Peer(final Socket socket, final InputStream is, final OutputStream os, final Map<String, String> headers) {
		this.socket = socket;
		this.is = is;
		this.os = os;
		this.headers = headers;
		this.peerID = UUID.randomUUID();
		this.bfr = new BasicFrameReader(is, HttpServer3.getDataLimitSize());
		this.bfw = new BasicFrameWriter(os);
		this.onConnected();
	}

	public Socket getSocket() {
		return this.socket;
	}

	public InputStream getInputStream() {
		return this.is;
	}

	public OutputStream getOutputStream() {
		return this.os;
	}

	public Map<String, String> getHeaders() {
		return Collections.unmodifiableMap(this.headers);
	}

	public UUID getUUID() {
		return this.peerID;
	}

	private final AtomicBoolean close = new AtomicBoolean();

	@Override
	public void run() {
		if (this.close.get())
			return;
		try {
			this.socket.setTrafficClass(0x10); /* IPTOS_LOWDELAY */
		} catch (Exception e) {
		}
		while (!this.close.get()) {
			try {
				final Frame frame = this.bfr.read();
				if (frame == null)
					continue;
				switch (frame.getFrameType()) {
					case CLOSE_FRAME: {
						this.onReceiveClose(frame);
						break;
					}
					case TEXT_FRAME:
					case CONTINUE_FRAME:
					case BINARY_FRAME: {
						this.onReceiveData(frame);
						break;
					}
					case PING_FRAME: {
						this.onReceivePing(frame);
						break;
					}
					case PONG_FRAME:
						break;
					default: {
						log.warn("Unknown frame type: " + frame.getFrameType());
						break;
					}
				}
			} catch (Exception e) {
				if (e instanceof IOException && e.getMessage() != null && e.getMessage().contains("Stream closed"))
					break;
				if (e instanceof EOFException)
					break;
				log.error(e);
			}
		}
	}

	private boolean isStart = true, isTextMessage;
	private byte[] buffer = new byte[0];

	private void onReceiveData(final Frame frame) {
		if (isStart && frame.getFrameType() == FrameType.TEXT_FRAME) {
			isTextMessage = true;
		} else if (isStart && frame.getFrameType() == FrameType.BINARY_FRAME) {
			isTextMessage = false;
		} else if (isStart) {
			// wrong frame since at the start of the message, message type has to be present
			throw new InvalidFrameException();
		}
		// if a frame is not the start of fragmented message, then the type has to be
		// CONINTUE
		if (!isStart && frame.getFrameType() != FrameType.CONTINUE_FRAME)
			throw new InvalidFrameException();
		int length = buffer.length + frame.getPayload().length;
		if (length > bfr.getMaxBufferSize())
			throw new BufferOverflow();
		byte[] tmp = new byte[length];
		System.arraycopy(buffer, 0, tmp, 0, buffer.length);
		System.arraycopy(frame.getPayload(), 0, tmp, buffer.length, frame.getPayload().length);
		buffer = tmp;
		isStart = false;
		if (frame.isFinalSegment()) {
			byte[] data = buffer;
			isStart = true;
			buffer = new byte[0];
			if (isTextMessage) {
				if (ByteUtil.wsValidate(data)) {
					this.onTextMessage(new String(data));
				} else {
					throw new WebsocketException(CloseReason.CloseCodes.NOT_CONSISTENT);
				}
			} else {
				this.onBinaryMessage(data);
			}
		}
	}

	private void onReceiveClose(Frame frame) {
		CloseReason.CloseCode closeCode = null;
		if (frame.getPayload() != null && frame.getPayload().length == 1)
			throw new InvalidFrameException();
		try {
			if (frame.getPayload() != null && frame.getPayload().length != 0) {
				closeCode = CloseReason.CloseCodes.getCloseCode(ByteBuffer.wrap(ArrayUtils.addAll(new byte[]
					{ 0, 0 }, new byte[]
					{ frame.getPayload()[0], frame.getPayload()[1] })).getInt());
				// check valid closecode
				if (!validateCloseCode(closeCode))
					throw new InvalidFrameException();
				if (frame.getPayload().length > 2 && !ByteUtil.wsValidate(Arrays.copyOfRange(frame.getPayload(), 2, frame.getPayload().length - 2))) {
					throw new InvalidFrameException();
				}
			}
			this.sendClose(closeCode);
			Thread.sleep(50L);
			IOUtils.closeAllQuietly(this.is, this.os, this.bfw, this.bfr);
			this.close.set(true);
		} catch (IOException e) {
		} catch (InterruptedException e) {
			JVM.errorln(e);
		} catch (IllegalArgumentException e) {
			throw new InvalidFrameException();
		}
		if (closeCode == null)
			closeCode = CloseReason.CloseCodes.NO_STATUS_CODE;
		this.onClose(new CloseReason(closeCode, "received close request from other endpoint"));
	}

	private boolean validateCloseCode(CloseReason.CloseCode closeCode) {
		return (closeCode != CloseReason.CloseCodes.NO_STATUS_CODE && closeCode != CloseReason.CloseCodes.RESERVED
				&& closeCode != CloseReason.CloseCodes.CLOSED_ABNORMALLY && closeCode != CloseReason.CloseCodes.TLS_HANDSHAKE_FAILURE
				&& closeCode instanceof CloseReason.CloseCodes)
				|| (!(closeCode.getCode() >= 0 && closeCode.getCode() <= 2999) && closeCode.getCode() >= 3000 && closeCode.getCode() <= 4999);
	}

	public abstract void onConnected();

	public abstract void onTextMessage(final String message);

	public abstract void onBinaryMessage(final byte[] message);

	public abstract void onClose(final CloseReason reason);

	private void splitAndWrite(byte[] data, FrameType frameType) throws IOException {
		SubArrayCollection arrayCollection = new SubArrayCollection(data, 125);
		for (int i = 0; i < arrayCollection.getSubArrays().size(); i++) {
			SubArrayCollection.SubArray subArray = arrayCollection.getSubArrays().get(i);
			boolean isFinal = i == arrayCollection.getSubArrays().size() - 1;
			FrameType type = null;
			if (i == 0) {
				type = frameType;
			} else {
				type = FrameType.CONTINUE_FRAME;
			}
			this.bfw.write(new BasicFrame(isFinal, false, false, false, false, type, (byte) subArray.getSubArray().length, null, subArray.getSubArray()));
		}
	}

	protected void onReceivePing(Frame frame) throws IOException {
		this.bfw.write(new PongFrame(frame.getPayload()));
	}

	public void sendCloseQuietly(CloseReason.CloseCode closeCode) {
		try {
			this.bfw.write(new CloseFrame(closeCode));
		} catch (Exception _ignored) {
		}
		IOUtils.closeAllQuietly(this.is, this.os, this.bfw, this.bfr);
	}

	public void sendCloseQuietly() {
		try {
			this.bfw.write(new CloseFrame());
		} catch (Exception _ignored) {
		}
		IOUtils.closeAllQuietly(this.is, this.os, this.bfw, this.bfr);
	}

	public void sendClose(CloseReason.CloseCode closeCode) throws IOException {
		this.bfw.write(new CloseFrame(closeCode));
		IOUtils.closeAllQuietly(this.is, this.os, this.bfw, this.bfr);
	}

	public void sendClose() throws IOException {
		this.bfw.write(new CloseFrame());
		IOUtils.closeAllQuietly(this.is, this.os, this.bfw, this.bfr);
	}

	public void write(final String data) throws IOException {
		if (data == null || data.isEmpty())
			return;
		final byte[] bytes = data.getBytes(CharsetUtil.UTF_8);
		splitAndWrite(bytes, FrameType.TEXT_FRAME);
	}

	public void write(final byte[] data) throws IOException {
		if (data == null || data.length == 0)
			return;
		splitAndWrite(data, FrameType.BINARY_FRAME);
	}

	@Override
	public String toString() {
		return "Peer[hash=" + MathUtil.toHex(super.hashCode()) + ",uuid=" + this.peerID + "]";
	}
}
